{
  "cells": [
    {
      "cell_type": "markdown",
      "id": "882c596f-7a9c-4456-9ce1-b7a4cb72d10b",
      "metadata": {
        "id": "882c596f-7a9c-4456-9ce1-b7a4cb72d10b"
      },
      "source": [
        "# Quantum Software Development Journey:\n",
        "# From Theory to Application with Classiq - Part 4"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "2dbea8e0-9710-4ee8-90f6-a04358d2e157",
      "metadata": {
        "id": "2dbea8e0-9710-4ee8-90f6-a04358d2e157"
      },
      "source": [
        "**Welcome to the Classiq Workshop Series for QClass 2024!**\n",
        "\n",
        "In this series, we will develop the skills needed to participate in quantum software development!\n",
        "\n",
        "- Week 1: Classiq's Basics & High-Level Functional Design\n",
        "- Week 2: Using Git as a Tool for In-Team Collaboration and Open Source Contributions\n",
        "- Week 3: Introduction to Quantum Machine Learning (QML) & VQE\n",
        "- **Week 4: QNN and and Advanced Applications**\n",
        "\n",
        "**[New Classiq's documentation](https://docs.classiq.io/latest/)!**\n",
        "\n",
        "\n",
        "\n",
        "\n",
        "Additional resources you should use are\n",
        "- The IDE of the classiq platform at [platform.classiq.io](platform.classiq.io)\n",
        "- The [community Slack of Classiq](https://short.classiq.io/join-slack) - Classiq's team will answer any question you have over there, including implementation questions\n",
        "- Course's [GitHub repository](https://github.com/Classiq/classiq-library/tree/main/community/QClass_2024)\n",
        "\n",
        "  \n",
        "**Good luck!**"
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "## Setting The Scene"
      ],
      "metadata": {
        "id": "p5c9gA_h9tZF"
      },
      "id": "p5c9gA_h9tZF"
    },
    {
      "cell_type": "markdown",
      "source": [
        "When running from Google Colab, we need to install Classiq's SDK and authenticate the remote device:"
      ],
      "metadata": {
        "id": "F5A8mXw389nQ"
      },
      "id": "F5A8mXw389nQ"
    },
    {
      "cell_type": "code",
      "source": [
        "# !pip install -U classiq"
      ],
      "metadata": {
        "id": "4oEdGj8m88xp"
      },
      "id": "4oEdGj8m88xp",
      "execution_count": 15,
      "outputs": []
    },
    {
      "cell_type": "code",
      "source": [
        "# import classiq\n",
        "# classiq.authenticate()"
      ],
      "metadata": {
        "id": "wUz9_4419L1H"
      },
      "id": "wUz9_4419L1H",
      "execution_count": 16,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "source": [
        "If you are working locally, make sure to install PyTorch according to your operating system using the official installation [website](https://pytorch.org/)."
      ],
      "metadata": {
        "id": "Ugp1GzuuBXQt"
      },
      "id": "Ugp1GzuuBXQt"
    },
    {
      "cell_type": "markdown",
      "id": "44ebd39d-edc7-47d6-8132-f98c662a3443",
      "metadata": {
        "id": "44ebd39d-edc7-47d6-8132-f98c662a3443"
      },
      "source": [
        "## Intro"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "a215faeb-363d-49e4-b2d9-34c945138ba0",
      "metadata": {
        "id": "a215faeb-363d-49e4-b2d9-34c945138ba0"
      },
      "source": [
        "In this example, we will show a simple example of parametric quantum program (PQC).\n",
        "\n",
        "We will take 1 input from the user, and consider 1 weight, while utilizing 1 qubit in the PQC. During this example, the goal of the learning process is to assess the right angle for a Rx gate for performing a \"NOT\" operation (spoiler, the correct answer is $\\pi$\n",
        ").\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "5f7de2e4-a645-44af-99a3-f3eb85473ec5",
      "metadata": {
        "id": "5f7de2e4-a645-44af-99a3-f3eb85473ec5"
      },
      "source": [
        "## General flow"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "d41dd8d0-6eca-4edc-8af5-5c5b7767008b",
      "metadata": {
        "id": "d41dd8d0-6eca-4edc-8af5-5c5b7767008b"
      },
      "source": [
        "In [section 1](#step-1-create-our-torchnnmodule) we will see the code required for defining a quantum layer.\n",
        "This will include:\n",
        "\n",
        "- section 1.1: defining the quantum model and synthesizing it to a quantum program\n",
        "- section 1.2: defining the execute and post-process callables\n",
        "- section 1.3: defining a `torch.nn.Module` network\n",
        "\n",
        "In section 2 we will choose our dataset, loss function, and optimizer.\n",
        "Section 3 will demostrate how to handle the learning process, and section 4 will test our network's performance.\n",
        "\n",
        "If you're not familiar with PyTorch, it is highly recommended that you'll check out the following pages from their documentation:\n",
        "\n",
        "- [Creating Models](https://pytorch.org/tutorials/beginner/basics/quickstart_tutorial.html#creating-models)\n",
        "- [Build the Neural Network](https://pytorch.org/tutorials/beginner/basics/buildmodel_tutorial.html)\n",
        "- [Optimizing the Model Parameters](https://pytorch.org/tutorials/beginner/basics/quickstart_tutorial.html#optimizing-the-model-parameters)\n",
        "- [Tensors](https://pytorch.org/tutorials/beginner/basics/tensorqs_tutorial.html)\n",
        "- [Datasets & DataLoaders](https://pytorch.org/tutorials/beginner/basics/data_tutorial.html)\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "ed136231-3910-45ca-9ec6-c33f5ef428cd",
      "metadata": {
        "id": "ed136231-3910-45ca-9ec6-c33f5ef428cd"
      },
      "source": [
        "# Step 1 - Create our `torch.nn.Module`"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "dae15b1c-0d3f-4807-ae48-edfba8425962",
      "metadata": {
        "id": "dae15b1c-0d3f-4807-ae48-edfba8425962"
      },
      "source": [
        "## Step 1.1 - Create our parametric quantum program"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "5105b43a-340e-4f79-8d54-f6e898ba415c",
      "metadata": {
        "id": "5105b43a-340e-4f79-8d54-f6e898ba415c"
      },
      "source": [
        "Our quantum model will be defined and synthesized as follows:"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 17,
      "id": "16b7cd0b-5438-4ec8-ae83-3b17b450bbe5",
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "16b7cd0b-5438-4ec8-ae83-3b17b450bbe5",
        "outputId": "817659cc-4556-49b3-cdf9-cc6cb33c35ee"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Opening: https://platform.classiq.io/circuit/9087e174-1b7d-4e73-a0ba-dc5e37ad097b?version=0.41.2\n"
          ]
        }
      ],
      "source": [
        "from classiq import synthesize, qfunc, QArray, QBit, RX, Output, allocate, CReal, show\n",
        "from classiq.qmod.quantum_function import create_model\n",
        "\n",
        "\n",
        "@qfunc\n",
        "def encoding(theta: CReal, q: QArray[QBit]) -> None:\n",
        "    RX(theta=theta, target=q[0])\n",
        "\n",
        "\n",
        "@qfunc\n",
        "def mixing(theta: CReal, q: QArray[QBit]) -> None:\n",
        "    RX(theta=theta, target=q[0])\n",
        "\n",
        "\n",
        "@qfunc\n",
        "def main(input_0: CReal, weight_0: CReal, res: Output[QArray[QBit]]) -> None:\n",
        "    allocate(1, res)\n",
        "    encoding(theta=input_0, q=res)  # Loading input\n",
        "    mixing(theta=weight_0, q=res)   # Adjustable parameter\n",
        "\n",
        "\n",
        "model = create_model(main)\n",
        "\n",
        "quantum_program = synthesize(model)\n",
        "show(quantum_program)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "7abccf3f-59a9-4ca4-9abd-a3f73f80efc0",
      "metadata": {
        "id": "7abccf3f-59a9-4ca4-9abd-a3f73f80efc0"
      },
      "source": [
        "The input (`input_0`), logically indicating the state `|0>` or `|1>`, is transformed into an angle, either `0` or `pi`.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "bef15105-d942-45a6-a615-7b5458241703",
      "metadata": {
        "id": "bef15105-d942-45a6-a615-7b5458241703"
      },
      "source": [
        "## Step 1.2 - Create the Execution and Post-processing"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "69875135-2a3d-4d8a-91df-ee501f01eac2",
      "metadata": {
        "id": "69875135-2a3d-4d8a-91df-ee501f01eac2"
      },
      "source": [
        "The following example defines a function that takes in a parametric quantum program plus parameters, executes the program, and returns the result. Notes:\n",
        "\n",
        "1. The code can be executed on a physical computer or on a simulator. In any case, implement the execution using `execute_qnn`.\n",
        "2. Post-process the result of the execution to obtain a single number (`float`) and a single dimension `Tensor`.\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 18,
      "id": "e86e8923-1b0a-48a3-a973-9a31773f08e6",
      "metadata": {
        "id": "e86e8923-1b0a-48a3-a973-9a31773f08e6"
      },
      "outputs": [],
      "source": [
        "import torch\n",
        "\n",
        "from classiq.applications.qnn.types import (\n",
        "    MultipleArguments,\n",
        "    SavedResult,\n",
        "    ResultsCollection,\n",
        ")\n",
        "\n",
        "from classiq.execution import execute_qnn\n",
        "from classiq.synthesis import SerializedQuantumProgram\n",
        "\n",
        "\n",
        "def execute(\n",
        "    quantum_program: SerializedQuantumProgram, arguments: MultipleArguments\n",
        ") -> ResultsCollection:\n",
        "    return execute_qnn(quantum_program, arguments)\n",
        "\n",
        "\n",
        "# Post-process the result, returning a dict:\n",
        "# Note: this function assumes that we only care about\n",
        "#   differentiating a single state (|0>)\n",
        "#   from all the rest of the states.\n",
        "#   In case of a different differentiation, this function should change.\n",
        "def post_process(result: SavedResult) -> torch.Tensor:\n",
        "    \"\"\"\n",
        "    Take in a `SavedResult` with `ExecutionDetails` value type, and return the\n",
        "    probability of measuring |0> which equals the amount of `|0>` measurements\n",
        "    divided by the total amount of measurements.\n",
        "    \"\"\"\n",
        "    counts: dict = result.value.counts\n",
        "    # The probability of measuring |0>\n",
        "    p_zero: float = counts.get(\"0\", 0.0) / sum(counts.values())\n",
        "    return torch.tensor(p_zero)\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "aa6a64b5-bdff-4e66-b16c-6ede1d69877d",
      "metadata": {
        "id": "aa6a64b5-bdff-4e66-b16c-6ede1d69877d"
      },
      "source": [
        "## Step 1.3 - Create a network"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "1937b4d5-abfa-40bd-b26e-32c93936be18",
      "metadata": {
        "id": "1937b4d5-abfa-40bd-b26e-32c93936be18"
      },
      "source": [
        "Now we're going to define a network, just like any other PyTorch network, only that this time, we will have only 1 layer, and it will be a quantum layer."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 19,
      "id": "9731c78f-ed71-47e8-9529-dcbb7e6d0bdf",
      "metadata": {
        "id": "9731c78f-ed71-47e8-9529-dcbb7e6d0bdf"
      },
      "outputs": [],
      "source": [
        "import torch\n",
        "\n",
        "from classiq.applications.qnn import QLayer\n",
        "\n",
        "\n",
        "class Net(torch.nn.Module):\n",
        "    def __init__(self, *args, **kwargs) -> None:\n",
        "        super().__init__()\n",
        "        self.qlayer = QLayer(\n",
        "            quantum_program,  # the quantum program, the result of `synthesize()`\n",
        "            execute,  # a callable that takes\n",
        "            # - a quantum program\n",
        "            # - parameters to that program (a tuple of dictionaries)\n",
        "            # and returns a `ResultsCollection`\n",
        "            post_process,  # a callable that takes\n",
        "            # - a single `SavedResult`\n",
        "            # and returns a `torch.Tensor`\n",
        "            *args,\n",
        "            **kwargs\n",
        "        )\n",
        "\n",
        "    def forward(self, x: torch.Tensor) -> torch.Tensor:\n",
        "        x = self.qlayer(x)\n",
        "        return x\n",
        "\n",
        "\n",
        "model = Net()"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "a94ff1bc-58f5-4743-aa09-0cc77dcfecd6",
      "metadata": {
        "id": "a94ff1bc-58f5-4743-aa09-0cc77dcfecd6"
      },
      "source": [
        "# Step 2 - Choose a dataset, loss function, and optimizer"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "8ac9fb68-bbf5-48c1-ba7a-2af0d9219b74",
      "metadata": {
        "id": "8ac9fb68-bbf5-48c1-ba7a-2af0d9219b74"
      },
      "source": [
        "We will use the DATALOADER_NOT dataset, defined [here](https://docs.classiq.io/latest/reference-manual/built-in-algorithms/qml/qnn/datasets/) as well as [L1Loss](https://pytorch.org/docs/stable/generated/torch.nn.L1Loss.html) and [SGD](https://pytorch.org/docs/stable/generated/torch.optim.SGD.html)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 20,
      "id": "a5d47c00-3040-43d2-b3d6-1d5a1614a2ee",
      "metadata": {
        "id": "a5d47c00-3040-43d2-b3d6-1d5a1614a2ee"
      },
      "outputs": [],
      "source": [
        "from classiq.applications.qnn.datasets import DATALOADER_NOT\n",
        "import torch.nn as nn\n",
        "import torch.optim as optim\n",
        "\n",
        "_LEARNING_RATE = 1\n",
        "\n",
        "# choosing our data\n",
        "data_loader = DATALOADER_NOT\n",
        "# choosing our loss function\n",
        "loss_func = nn.L1Loss()     # Mean Absolute Error (MAE)\n",
        "# choosing our optimizer\n",
        "optimizer = optim.SGD(model.parameters(), lr=_LEARNING_RATE)\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "b0087ab6-addd-41ac-b609-f60b0b4591cd",
      "metadata": {
        "id": "b0087ab6-addd-41ac-b609-f60b0b4591cd"
      },
      "source": [
        "# Step 3 - Train"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "ae072cf7-431f-4583-b5c7-a21be4ef56f2",
      "metadata": {
        "id": "ae072cf7-431f-4583-b5c7-a21be4ef56f2"
      },
      "source": [
        "For the training process, we will use a loop similar to [the one recommended by PyTorch](https://pytorch.org/tutorials/beginner/blitz/neural_networks_tutorial.html#update-the-weights)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 21,
      "id": "bab03c43-9d90-4d3b-9ece-4b2f8655be33",
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "bab03c43-9d90-4d3b-9ece-4b2f8655be33",
        "outputId": "2801de37-411f-4fa8-d9c7-bb9d3fa0db87"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "0 Parameter containing:\n",
            "tensor([0.1470], requires_grad=True)\n",
            "1 Parameter containing:\n",
            "tensor([0.3423], requires_grad=True)\n",
            "2 Parameter containing:\n",
            "tensor([0.5132], requires_grad=True)\n",
            "3 Parameter containing:\n",
            "tensor([0.6474], requires_grad=True)\n",
            "4 Parameter containing:\n",
            "tensor([0.9892], requires_grad=True)\n",
            "5 Parameter containing:\n",
            "tensor([0.9282], requires_grad=True)\n",
            "6 Parameter containing:\n",
            "tensor([1.0137], requires_grad=True)\n",
            "7 Parameter containing:\n",
            "tensor([1.0747], requires_grad=True)\n",
            "8 Parameter containing:\n",
            "tensor([1.5386], requires_grad=True)\n",
            "9 Parameter containing:\n",
            "tensor([2.1977], requires_grad=True)\n",
            "10 Parameter containing:\n",
            "tensor([2.8691], requires_grad=True)\n",
            "11 Parameter containing:\n",
            "tensor([3.0522], requires_grad=True)\n",
            "12 Parameter containing:\n",
            "tensor([3.0034], requires_grad=True)\n",
            "13 Parameter containing:\n",
            "tensor([3.1743], requires_grad=True)\n",
            "14 Parameter containing:\n",
            "tensor([3.1499], requires_grad=True)\n",
            "15 Parameter containing:\n",
            "tensor([3.1499], requires_grad=True)\n",
            "16 Parameter containing:\n",
            "tensor([3.1499], requires_grad=True)\n",
            "17 Parameter containing:\n",
            "tensor([3.1499], requires_grad=True)\n",
            "18 Parameter containing:\n",
            "tensor([3.1499], requires_grad=True)\n",
            "19 Parameter containing:\n",
            "tensor([3.1499], requires_grad=True)\n"
          ]
        }
      ],
      "source": [
        "import torch.nn as nn\n",
        "import torch.optim as optim\n",
        "from torch.utils.data import DataLoader\n",
        "\n",
        "\n",
        "def train(\n",
        "    model: nn.Module,\n",
        "    data_loader: DataLoader,\n",
        "    loss_func: nn.modules.loss._Loss,\n",
        "    optimizer: optim.Optimizer,\n",
        "    epoch: int = 20,\n",
        ") -> None:\n",
        "    for index in range(epoch):\n",
        "        print(index, model.qlayer.weight)\n",
        "        for data, label in data_loader:\n",
        "            optimizer.zero_grad()\n",
        "\n",
        "            output = model(data)\n",
        "\n",
        "            loss = loss_func(output, label)\n",
        "            loss.backward()\n",
        "\n",
        "            optimizer.step()\n",
        "\n",
        "\n",
        "train(model, data_loader, loss_func, optimizer)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "61fa4f83-8bd2-4290-9ca4-f6c5c1ca0e32",
      "metadata": {
        "id": "61fa4f83-8bd2-4290-9ca4-f6c5c1ca0e32"
      },
      "source": [
        "# Step 4 - Test"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "7533b162-6214-4606-9e66-ec3332a2f446",
      "metadata": {
        "id": "7533b162-6214-4606-9e66-ec3332a2f446"
      },
      "source": [
        "Lastly, we will test our network accuracy, using [the following answer](https://stackoverflow.com/questions/52176178/pytorch-model-accuracy-test#answer-64838681)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 22,
      "id": "010caccd-d1ca-4aac-88a4-d115a63c7c7d",
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "010caccd-d1ca-4aac-88a4-d115a63c7c7d",
        "outputId": "36e7fdd7-5bdf-4050-f8c4-a929c719e8b8"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Test Accuracy of the model: 100.00%\n"
          ]
        },
        {
          "output_type": "execute_result",
          "data": {
            "text/plain": [
              "1.0"
            ]
          },
          "metadata": {},
          "execution_count": 22
        }
      ],
      "source": [
        "def check_accuracy(model: nn.Module, data_loader: DataLoader, atol=1e-4) -> float:\n",
        "    num_correct = 0\n",
        "    total = 0\n",
        "    model.eval()\n",
        "\n",
        "    with torch.no_grad():   # Temporarily disable gradient calculation\n",
        "        for data, labels in data_loader:\n",
        "            # Let the model predict\n",
        "            predictions = model(data)\n",
        "\n",
        "            # Get a tensor of booleans, indicating if each label is close to the real label\n",
        "            is_prediction_correct = predictions.isclose(labels, atol=atol)\n",
        "\n",
        "            # Count the amount of `True` predictions\n",
        "            num_correct += is_prediction_correct.sum().item()\n",
        "            # Count the total evaluations\n",
        "            #   the first dimension of `labels` is `batch_size`\n",
        "            total += labels.size(0)\n",
        "\n",
        "    accuracy = float(num_correct) / float(total)\n",
        "    print(f\"Test Accuracy of the model: {accuracy*100:.2f}%\")\n",
        "    return accuracy\n",
        "\n",
        "\n",
        "check_accuracy(model, data_loader)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "0bc93c12-6643-4623-9ce5-7984ba5ff429",
      "metadata": {
        "id": "0bc93c12-6643-4623-9ce5-7984ba5ff429"
      },
      "source": [
        "**The results show that the accuracy is $1$**, meaning a 100% success rate at performing the required transformation (i.e. the network learned to perform a X-gate). We may further test it by printing the value of `model.qlayer.weight`, which is a tensor of shape (1,1), which should, after training, be close to $\\pi$"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "eb4ce7b7-3410-491e-aaff-430b61f67157",
      "metadata": {
        "id": "eb4ce7b7-3410-491e-aaff-430b61f67157"
      },
      "source": [
        "# Summary"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "fadd4057-34e8-4103-8aca-a13026fea660",
      "metadata": {
        "id": "fadd4057-34e8-4103-8aca-a13026fea660"
      },
      "source": [
        "In this example, we wrote a fully working Quantum Neural Network from scratch, trained it, and saw its success at learning the requested transformation.\n",
        "\n",
        "In section 1 we defined our parametric quantum program, as well as our execution function. Together, these two are sent as arguments to [the QLayer object](https://docs.classiq.io/latest/reference-manual/built-in-algorithms/qml/qnn/qlayer/). In section 2 we set some hyperparameters, and in section 3 we trained our model. Section 4 helped us verify that our network is working as intended."
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "# Advanced Application: Quantum Auto-Encoder"
      ],
      "metadata": {
        "id": "EhRpt6iTcmYC"
      },
      "id": "EhRpt6iTcmYC"
    },
    {
      "cell_type": "code",
      "source": [],
      "metadata": {
        "id": "2t_yTMCu-ngV"
      },
      "id": "2t_yTMCu-ngV",
      "execution_count": null,
      "outputs": []
    }
  ],
  "metadata": {
    "kernelspec": {
      "display_name": "Python 3 (ipykernel)",
      "language": "python",
      "name": "python3"
    },
    "language_info": {
      "codemirror_mode": {
        "name": "ipython",
        "version": 3
      },
      "file_extension": ".py",
      "mimetype": "text/x-python",
      "name": "python",
      "nbconvert_exporter": "python",
      "pygments_lexer": "ipython3",
      "version": "3.11.8"
    },
    "colab": {
      "provenance": []
    }
  },
  "nbformat": 4,
  "nbformat_minor": 5
}